/*
 *   Copyright (C) 2009  Jens Kieselbach
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You may obtain a copy of the License at http://www.gnu.org/licenses/gpl.html
 */
package de.kieselbach.swing

import java.awt.*
import java.awt.event.*
import javax.swing.*
import javax.swing.border.*

class PaneBuilder extends BuilderSupport {

    final static int DEFAULT = 0
    final static int CLASSIC = 1
    final static int MODERN = 2
    final static int GNOME = 3

    // ---------------------------------------------------------------------------------

    static long dialogId = 0 

    static int defaultType = GNOME
    static int fillerWidth = 10
    static int paneSpace = 10
    static int verticalButtonAligment = SwingConstants.BOTTOM
    static int minButtonWidth = 80
	
    static Border paneBorder = BorderFactory.createEmptyBorder(10, 10, 5, 10)

    static okText = 'OK'
    static cancelText = 'Cancel'
    static closeText = 'Close'
    static yesText = 'Yes'
    static noText = 'No'
    static detailsText = 'Details'

    static Icon okIcon
    static Icon cancelIcon
    static Icon closeIcon
    static Icon detailIcon

    static Icon errorIcon
    static Icon infoIcon
    static Icon questionIcon
    static Icon warningIcon

    // ---------------------------------------------------------------------------------

    static void setDefaultType(def t) {

        if(t == null || (t != CLASSIC && t != MODERN && t != GNOME)) {
            throw new IllegalArgumentException('invalid type: ' + t)
        }
    }

    static void setFillerWidth(int w) {

        if(w == null || w < 0) {
            throw new IllegalArgumentException('invalid filler width: ' + w)
        }

        fillerWidth = w
    }

    static void setPaneSpace(int w) {

        if(w == null || w < 0) {
            throw new IllegalArgumentException('invalid pane space: ' + w)
        }

        paneSpace = w
    }

    // ---------------------------------------------------------------------------------

    def type
    def maxWidth = 0
    def maxHeight = 0

    def buttonsPerBar = [:] as Map
    def currentRootPane
    def componentsToBePacked = [] as Set
    def dialogsToBeCentered = [] as Set
    def dialogs = [:] as Map
    def dialogOptions = [:] as Map

    protected void setParent(Object parent, Object child) {

        if(child == null) {
            return
        }

        if (parent instanceof JPanel) {

            if(parent.name == 'pane') {
                if(child instanceof JPanel && (child.name == 'bar' || child.name == 'nogluebar')) {
                    parent.add(child, BorderLayout.SOUTH)
                }
                else{
                    parent.add(child as Component, BorderLayout.CENTER)
                }
            }
            else if(parent.name == 'bar' || parent.name == 'nogluebar') {
                if(child instanceof JButton) {

                    def d = parent.name == 'bar' ? 1 : 0
                    if(type == GNOME) {
                        parent.add(child, d)
                    }
                    else if(type == CLASSIC) {
                        parent.add(child, parent.componentCount - d)
                    }
                    else {
                        parent.add(child)
                    }

                    def buttons = buttonsPerBar[parent]
                    buttons.add(child)

                    if(buttons.size() > 0) {
                        def adjust = false
                        if(child.preferredSize.@width > maxWidth) {
                            maxWidth = child.preferredSize.@width
                            adjust = true
                        }
                        if(child.preferredSize.@height > maxHeight) {
                            maxHeight = child.preferredSize.@height
                            adjust = true
                        }

                        if(adjust) {
                            def size = [maxWidth, maxHeight + 10] as Dimension

                            buttons.each() {button ->
                                button.preferredSize = size
                            }
                        }
                        else {
                            child.preferredSize = [maxWidth, maxHeight] as Dimension
                        }
                    }
                }
                else {

                    def d = parent.name == 'bar' ? 1 : 0
                    if(type == GNOME) {
                        parent.add(child as Component, d)
                    }
                    else if(type == CLASSIC) {
                        parent.add(child as Component, parent.componentCount - d)
                    }
                    else {
                        parent.add(child as Component)
                    }
                }
            }
            else {
                parent.add(child as Component)
            }
        }
        else if (parent instanceof JDialog) {
            parent.contentPane = child as Container
        }
    }

    @Override
    protected void nodeCompleted(Object parent, Object node) {

        if(node instanceof JDialog) {
            if(componentsToBePacked.remove(node)) {
                node.pack()
            }

            if(dialogsToBeCentered.remove(node)) {
                UIUtilities.centerDialog(node.owner, node)
            }

            currentRootPane = null
        }
        else if (node instanceof JPanel && node.name == 'bar') {
            buttonsPerBar.remove(node)
        }
    }

    protected Object createNode(Object name) {

        return createNode(name, null, null)
    }

    protected Object createNode(Object name, Object value) {

        return createNode(name, null, value)
    }

    protected Object createNode(Object name, Map attributes) {

        return createNode(name, attributes, null)
    }

    protected Object createNode(Object name, Map attributes, Object value) {

        def node

        if(name == 'pane') {
            node = createPane(name, attributes, value)
        }
        else if(name == 'bar') {
            node = createBar(name, attributes, value)
        }
        else if(name == 'button' || name == 'ok' || name == 'cancel' || name == 'close' || name == 'yes' || name == 'no') {
            node = createButton(name, attributes, value)
        }
        else if(name == 'filler') {
            node = createFiller(name, attributes, value)
        }
        else if(name == 'glue') {
            node = createGlue(name, attributes, value)
        }
        else if(name == 'widget') {
            node = createWidget(name, attributes, value)
        }
        else if(name == 'dialog') {
            node = createDialog(name, attributes, value)
        }
        else if(name == 'question' || name == 'information' || name == 'warning' || name == 'error' || name == 'plain') {
            node = createMessageDialog(name, attributes, value)
        }
        else {
            throw new IllegalArgumentException('unknown node name: ' + name)
        }

        node
    }

    // ------------------------------------------------------------------------

    private Object createPane(Object name, Map attributes, Object value) {

        def node = new JPanel(new BorderLayout(paneSpace, paneSpace))
        node.border = paneBorder
        node.name = 'pane'

        if(attributes != null) {
            attributes.each() {key, val ->

                if(key == 'layout') {
                    throw new IllegalArgumentException('setting the layout of a pane is not allowed!')
                }
                else {
                    node[key] = val
                }
            }
        }

        node
    }

    private Object createBar(Object name, Map attributes, Object value) {

        def node = new JPanel()
        node.layout = new BoxLayout(node, BoxLayout.X_AXIS)

        type = defaultType
        def glue = true

        if(attributes != null) {
            attributes.each() {key, val ->

                if(key == 'type') {
                    if(val != DEFAULT && val != CLASSIC && val != MODERN && val != GNOME) {
                        throw new IllegalArgumentException('invalid type: ' + val)
                    }

                    if(val != DEFAULT) {
                        type = val as int
                    }
                }
                else if(key == 'glue') {
                    glue = val
                }
                else if(key == 'layout') {
                    throw new IllegalArgumentException('setting the layout of a bar is not allowed!')
                }
                else {
                    node[key] = val
                }
            }
        }

        if(glue) {
            node.add(Box.createHorizontalGlue())
            if(type == CLASSIC) {
                node.add(Box.createHorizontalGlue())
            }

            node.name = 'bar'
        }
        else {
            node.name = 'nogluebar'
        }

        buttonsPerBar[node] = [] as LinkedList

        node
    }

    private Object createButton(Object name, Map attributes, Object value) {

       def node = new JButton()

        if(name == 'ok') {
            node.text = okText
            node.icon = okIcon
        }
        else if(name == 'cancel') {
            node.text = cancelText
            node.icon = cancelIcon
        }
        else if(name == 'close') {
            node.text = closeText
            node.icon = closeIcon
        }
        else if(name == 'yes') {
            node.text = yesText
            node.icon = okIcon
        }
        else if(name == 'no') {
            node.text = noText
            node.icon = cancelIcon
        }

        node.verticalAlignment = verticalButtonAligment

        if(value != null) {
            node.text = value as String
        }

        if(attributes != null) {
            attributes.each() {key, val ->

                if(key == 'default') {
                    if(val && currentRootPane != null) {
                        currentRootPane.defaultButton = node
                    }
                }
                else if(key == 'validator') {
                    if(val != null) {
                        val.enable(node)
                    }
                }
                else if(key == 'activator') {
                    if(val != null) {
                        val.add(node)
                    }
                }
                else {
                    node[key] = val
                }
            }
        }
		
		if(node.preferredSize.@width < minButtonWidth) {
			node.preferredSize = [minButtonWidth, node.preferredSize.@height] as Dimension
		}

        node
    }

    private Object createFiller(Object name, Map attributes, Object value) {

        int w = fillerWidth
        if(value != null) {
            w = value as int
            if(w < 0) {
                throw new IllegalArgumentException('invalid filler width: ' + w)
            }
        }

        Box.createHorizontalStrut(w)
    }

    private Object createGlue(Object name, Map attributes, Object value) {

        Box.createHorizontalGlue()
    }

    private Object createWidget(Object name, Map attributes, Object value) {

        def node = null
        if(value != null) {
            node = value

            if(attributes != null) {
                attributes.each() {key, val ->
                    node[key] = val
                }
            }
        }

        node
    }

    private Object createDialog(Object name, Map attributes, Object value) {

        def owner = null
        def modal = false

        if(attributes != null) {
            owner = attributes['owner']
            def m = attributes['modal']
            if(m != null) {
                modal = m
            }
        }

        def node
        if(owner == null) {
            node = new JDialog()
        }
        else if(owner instanceof Dialog) {
            node = new JDialog(owner as Dialog, modal as boolean)
        }
        else if(owner instanceof Frame) {
            node = new JDialog(owner as Frame, modal as boolean)
        }
        else if(owner instanceof Window) {
            node = new JDialog(owner as Window)
        }
        else {
            throw new IllegalArgumentException('invalid owner: ' + owner)
        }

        if(value != null) {
            node.title = value as String
        }

        if(attributes != null) {
            attributes.each() {key, val ->

                if(key == 'pack') {
                    if(val) {
                        componentsToBePacked.add(node)
                    }
                }
                else if(key == 'center') {
                    if(val) {
                        dialogsToBeCentered.add(node)
                    }
                }
                else if(key != 'owner' && key != 'modal') {
                    node[key] = val
                }
            }
        }

        currentRootPane = node.rootPane

        node
    }

    private Object createMessageDialog(Object name, Map attributes, Object value) {

        def ownerwin = null
        def options = null
        def defoption = null
        def exception = null

        def message = new JPanel(new BorderLayout(paneSpace, paneSpace))

        def useicon
        switch(name) {
            case 'question':
                useicon = questionIcon
                break
            case 'information':
                useicon = infoIcon
                break
            case 'warning':
                useicon = warningIcon
                break
            case 'error':
                useicon = errorIcon
                break
            case 'plain':
                useicon = null
                break

            default:
                throw new IllegalArgumentException('invalid name: ' + name)
        }

        if(useicon != null) {
            def ip = new JPanel(new BorderLayout())
            ip.border = BorderFactory.createEmptyBorder(2, 0, 0, 0)
            message.add(ip, BorderLayout.WEST)
            ip.add(new JLabel(useicon), BorderLayout.NORTH)
        }
        
        if(attributes != null) {
            ownerwin = attributes['owner']
            exception = attributes['exception']

            if(name == 'question' || name == 'plain') {
                options = attributes['options']
                defoption = attributes['default']
            }
            else {
                options = ['ok']
                defoption = 'ok'
            }

            def msg = attributes['message']
            if(msg != null) {
                if(msg instanceof String) {
                    def labels = new JPanel(new GridLayout(0, 1, 2, 2))
                    msg.eachLine() {line ->
                        def l = new JLabel(line as String)
                        def ps = l.preferredSize
                        ps.@width = ps.@width + 5 
                        l.preferredSize = ps
                        labels.add(l)
                    }
                    message.add(labels, BorderLayout.CENTER)
                }
                else {
                    message.add(msg as Component, BorderLayout.CENTER)
                }
            }
        }

        def input = attributes['input']
        if(input != null) {
            message.add(input as Component, BorderLayout.SOUTH);
        }

        def tracepanel = null
        def tracescroll = null

        if(exception != null) {
            tracepanel = new JPanel(new BorderLayout(paneSpace, paneSpace))

            tracepanel.add(message, BorderLayout.CENTER);

            def tracearea = new JTextArea()
            tracearea.editable = false
            tracearea.font = new Font(Font.MONOSPACED, Font.PLAIN, 11)
            tracearea.tabSize = 4

            tracescroll = new JScrollPane(tracearea)
            tracescroll.visible = false
            tracescroll.preferredSize = [50,150] as Dimension
            tracepanel.add(tracescroll, BorderLayout.SOUTH);

            tracearea.append(exception.toString())
            tracearea.append('\n')
            exception.stackTrace.each() {elem ->
                tracearea.append('\t');
                tracearea.append(elem.toString())
                tracearea.append('\n')
            }

            if(exception.cause != null) {
                tracearea.append('Caused by: ');
                tracearea.append(exception.cause.toString())
                tracearea.append('\n')
                exception.cause.stackTrace.each() {elem ->
                    tracearea.append('\t');
                    tracearea.append(elem.toString())
                    tracearea.append('\n')
                }
            }

            tracearea.caretPosition = 0
        }

        def id = dialogId++
        def dlg = null
        def contentpane = null
        def defbutton = null

        dlg = dialog(value, owner: ownerwin, modal: true, resizable: false, pack: true, center: true) {
            contentpane = pane() {
                if(tracepanel != null) {
                    widget(tracepanel)
                }
                else if(message != null) {
                    widget(message)
                }
                bar(type: PaneBuilder.DEFAULT, glue: exception == null)  {

                    if(options != null && options.size() > 0) {
                        def num = 0
                        options.each() {opt ->

                            if(num > 0) {
                                filler()
                            }
                            num++

                            def isdef = opt == defoption
                            switch(opt) {
                                case 'yes':
                                    def button = yes(default: isdef, actionPerformed: {
                                        dialogs[id].visible = false
                                        dialogOptions[id] = 'yes'
                                    })

                                    if(isdef) {
                                        defbutton = button
                                    }
                                    break

                                case 'no':
                                    def button = no(default: isdef, actionPerformed: {
                                        dialogs[id].visible = false
                                        dialogOptions[id] = 'no'
                                    })

                                    if(isdef) {
                                        defbutton = button
                                    }
                                    break

                                case 'ok':
                                    def button = ok(default: isdef, actionPerformed: {
                                        dialogs[id].visible = false
                                        dialogOptions[id] = 'ok'
                                    })

                                    if(isdef) {
                                        defbutton = button
                                    }
                                    break

                                case 'cancel':
                                    def button = cancel(default: isdef, actionPerformed: {
                                        dialogs[id].visible = false
                                        dialogOptions[id] = 'cancel'
                                    })

                                    if(isdef) {
                                        defbutton = button
                                    }
                                    break

                                default:
                                    throw new IllegalArgumentException('invalid option: ' + opt)
                            }
                        }
                    }
                    else {
                        defbutton = ok(default: true, actionPerformed: {
                            dialogs[id].visible = false
                            dialogOptions[id] = 'ok'
                        })
                        filler()
                        cancel(actionPerformed: {
                            dialogs[id].visible = false
                            dialogOptions[id] = 'cancel'
                        })
                    }

                    if(exception != null) {
                        glue()
                        button(detailsText, icon: detailIcon, actionPerformed: {
                            if(tracescroll != null) {
                                tracescroll.visible = !tracescroll.visible
                                dlg.pack()
                                dlg.validate()
                                dlg.repaint()
                            }
                        })
                    }
                }
            }
        }

        dialogs[id] = dlg
        def focus = attributes['focus']
        if(focus != null) {
            focus.requestFocus()
        }
        else if(defbutton != null) {
            defbutton.requestFocus()
        }

        if(defbutton != null) {
            dlg.rootPane.defaultButton = defbutton
            def validator = attributes['validator']
            if(validator != null) {
                validator.enable(defbutton)
            }
        }

        contentpane.registerKeyboardAction({dlg.visible = false} as ActionListener, KeyStroke.getKeyStroke('ESCAPE'), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)

        dlg.visible = true
        dlg.dispose()

        dialogs.remove(id)
        return dialogOptions.remove(id)
    }

    // ------------------------------------------------------------------------------------------------------

    static void runWithMonitor(def parent, String progressmsg, String title, String msg, Closure run)  {

        def lnf = JDialog.isDefaultLookAndFeelDecorated()
        JDialog.setDefaultLookAndFeelDecorated(false)

        def dlg
        if(parent == null) {
            dlg = new JDialog()
        }
        else if(parent instanceof Dialog) {
            dlg = new JDialog(parent as Dialog, true)
        }
        else if(parent instanceof Frame) {
            dlg = new JDialog(parent as Frame, true)
        }
        else if(parent instanceof Window) {
            dlg = new JDialog(parent as Window)
        }
        else {
            throw new IllegalArgumentException('invalid parent: ' + parent)
        }

        JDialog.setDefaultLookAndFeelDecorated(lnf)
        dlg.resizable = false
        dlg.undecorated = true

        def main = new JPanel(new BorderLayout(5, 5))
        dlg.contentPane = main
        main.border = BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED), BorderFactory.createEmptyBorder(10, 10, 10, 10))
        def label = new JLabel(progressmsg)
		label.font = label.font.deriveFont(Font.BOLD)
        main.add(label, BorderLayout.NORTH)
        def bar = new JProgressBar(SwingConstants.HORIZONTAL)
        bar.preferredSize = [300, bar.preferredSize.@height] as Dimension
        bar.indeterminate = true
        main.add(bar, BorderLayout.CENTER)

        def JButton cancel = cancelIcon != null ? new JButton(cancelIcon) : new JButton('X')
        cancel.preferredSize = [cancel.preferredSize.@height, cancel.preferredSize.@height] as Dimension
        main.add(cancel, BorderLayout.EAST)

        dlg.pack()
        UIUtilities.centerDialog(parent, dlg)

        def thread = Thread.start() {
            try {
                run()

                SwingUtilities.invokeLater {
                    dlg.visible = false
                    dlg.dispose()
                }
            }
            catch(Throwable exc) {
                SwingUtilities.invokeLater {
                    dlg.visible = false
                    dlg.dispose()

                    new PaneBuilder().error(title, message: msg, owner: parent, exception: exc)
                }
            }
        }

        cancel.actionPerformed = {
            thread.interrupt()
        }

        dlg.visible = true
    }    
}